[免杀学习]shellcode的使用
刚开始学免杀,想直接分析shellcode的话对我来说有点难,所以先学会如何使用别人写好的shellcode
ShellCode准备
MSF生成的在x64的win环境下打开calc.exe的shellcode
1 | msfvenom -p windows/x64/exec CMD=calc.exe -f c |
完整代码
1 |
|
注释的代码是另一种写法,但可以达到同样的效果,在做免杀的可以考虑等效替换,对于关键字匹配的杀毒引擎来说可能会有免杀的效果
介绍一下涉及到的函数
VirtualAlloc
功能:调整一片在虚拟地址空间的内存的状态
返回值:调指向整的内存的起始地址的指针
win api为了准确的表达指针的作用对象,将void命名为了许多其他类型,但其本质都是void
void类型是一种无类型指针,也就是说它可以指向任意类型的数据
**void类型的指针有以下几种用途:
- 用作函数的返回类型,表示函数不返回任何值。
- 用作函数的参数类型,表示函数可以接受任意类型的指针。例如,内存分配函数 malloc 和 memset 的参数就是 void* 类型。
- 用作通用指针,表示可以指向任何未使用 const 或 volatile 关键字声明的变量。例如,可以把 int* 类型的指针赋值给 void* 类型的指针,但是反过来就需要强制类型转换
1 | LPVOID VirtualAlloc( |
virtualAlloc 函数 (memoryapi.h) - Win32 apps | Microsoft Learn
官方文档讲的对初学者不太友好,我就自己总结一下
LPVOID lpAddress:要分配的内存的地址,如果不指定就让系统自动分配
SIZE_T dwSize:要分配的内存的大小,单位是字节
DWORD flAllocationType:要分配的内存的类型,可以是保留,提交,重置,撤销等。(保留和提交是主要使用的)
内存类型 | 说明 |
---|---|
保留(MEM_RESERVE) | 在虚拟地址空间中预留一块区域,但是不分配实际的物理内存。这样可以保证这块区域不会被其他程序占用,但是也不能直接使用。 |
提交(MEM_COMMIT) | 在保留的区域中分配实际的物理内存,这样才能真正使用这块内存。提交的内存会被初始化为零。 |
重置(MEM_RESET) | 把已经提交的内存标记为不再需要,系统可以随时回收这些内存。重置的内存不会被释放,但是也不能直接使用,需要重新提交才能使用。 |
撤销(MEM_RESET_UNDO) | 把已经重置的内存标记为重新需要,系统会尝试恢复这些内存的内容。如果成功,那么撤销的内存可以继续使用;如果失败,那么撤销的内存会变成零,需要重新写入数据才能使用 |
DWORD flProtect:用来指定要分配的内存的保护属性的,也就是说,它可以控制这块内存可以被怎样访问,比如只读、可写、可执行等。
保护属性 | 含义 |
---|---|
PAGE_EXECUTE | 允许对已提交的内存执行代码 |
PAGE_EXECUTE_READ | 允许对已提交的内存执行代码或只读访问 |
PAGE_EXECUTE_READWRITE | 允许对已提交的内存执行代码或读写访问 |
PAGE_EXECUTE_WRITECOPY | 允许对文件映射对象的映射视图执行代码或复制写入访问 |
PAGE_NOACCESS | 禁止对已提交的内存进行任何访问 |
PAGE_READONLY | 允许对已提交的内存进行只读访问 |
PAGE_READWRITE | 允许对已提交的内存进行读写访问 |
PAGE_WRITECOPY | 允许对文件映射对象的映射视图进行只读或复制写入访问 |
RtlMoveMemory
memcpy
这两个都是用于内存复制的方法,很容易理解,直接看官方文档即可
RtlMoveMemory 函数 - Win32 apps | Microsoft Learn
还有类似的内存复制方法如RtlCopyMemory
RtlCopyMemory 宏 (ntddstor.h) - Windows drivers | Microsoft Learn
VirtualProtect
功能:改变一段内存区域的保护属性
1 | BOOL VirtualProtect( |
LPVOID lpAddress:内存区域的指针
SIZE_T dwSize:要改变保护属性的区域大小
DWORD flNewProtect:要设定的区域的保护属性
PDWORD lpflOldProtect:保存旧的保护属性的指针
调用内存中的shellcode(1)
shellcode的本质是汇编语言转换成的二进制代码
1 | // 分配1MB大小的内存空间,保留并提交,可读可写可执行 |
((void(*)())lpMem)();的含义是将lpMem指针强制转换为一个无参数无返回值的函数指针,然后调用这个函数。
具体来说,((void(*)())lpMem)(); 可以分解为以下几个步骤:
- lpMem是一个LPVOID类型的指针,它指向一段分配好的内存区域,其中存放了buf数组的内容,也就是一段机器码。
- (void(*)())是一个类型转换符,它表示一个无参数无返回值的函数指针类型。
- (void(*)())lpMem是将lpMem指针强制转换为(void(*)())类型,也就是将内存区域视为一个函数。
- ((void(*)())lpMem)()是在转换后的函数指针后面加上一对括号,表示调用这个函数。
这样做的目的是执行buf数组中的机器码,也就是一段shellcode。
shellcode可以转换为一个无参数无返回值的函数调用,是因为它的设计和编码方式使得它可以在任何内存地址上执行,而不需要依赖于参数或返回值。
具体来说,shellcode有以下几个特点:
- shellcode是一段可以直接在CPU上运行的二进制代码,它不需要经过编译器或链接器的处理,也不需要遵循任何函数调用约定。
- shellcode通常使用相对寻址或寄存器寻址的方式来访问数据或代码,而不使用绝对寻址或基址寻址,这样可以避免地址硬编码的问题。
- shellcode通常使用系统调用或API函数来实现功能,而不使用自己编写的函数或库函数,这样可以减少代码的长度和复杂度。
- shellcode通常在执行完毕后返回到原来的执行流程,而不是退出程序或造成异常,这样可以隐藏自己的存在。
因此,shellcode可以被视为一个无参数无返回值的函数,只要将它存放在一段可执行的内存区域中,并通过((void(*)())lpMem)(); 这种语法来调用它,就可以实现执行shellcode的目的。
调用内存中的shellcode(2)
1 | HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)lpMem, NULL, 0, NULL);//创建一个执行shellcode的线程 |
CreateThread
1 | HANDLE CreateThread( |
参数 | 类型 | 描述 |
---|---|---|
lpThreadAttributes | LPSECURITY_ATTRIBUTES | 指向 SECURITY_ATTRIBUTES 结构的指针,该结构确定返回的句柄是否可以由子进程继承。如果为 NULL,则无法继承句柄。结构的 lpSecurityDescriptor 成员为新线程指定安全描述符。如果为 NULL,则线程将获取默认的安全描述符。 |
dwStackSize | SIZE_T | 堆栈的初始大小(以字节为单位)。系统将此值舍入到最近的页面。如果此参数为零,新线程将使用可执行文件的默认大小。 |
lpStartAddress | LPTHREAD_START_ROUTINE | 指向由线程执行的应用程序定义函数的指针。此指针表示线程的起始地址。 |
lpParameter | LPVOID | 指向要传递给线程函数的变量的指针。如果不需要传递参数,则为 NULL。 |
dwCreationFlags | DWORD | 控制线程创建的标志。如果为 0,则创建后,线程会立即运行。如果为 CREATE_SUSPENDED,则线程以挂起状态创建,在调用 ResumeThread 函数之前不会运行。如果为 STACK_SIZE_PARAM_IS_A_RESERVATION,则 dwStackSize 参数指定堆栈的初始保留大小,而不是提交大小。 |
lpThreadId | LPDWORD | 指向接收线程标识符的变量的指针。如果为 NULL,则不返回线程标识符。 |
对于第三个参数,LPTHREAD_START_ROUTINE其实也是是一个函数指针的类型,只不过它表明这个函数指针应该是是由一个线程执行的。
所以(LPTHREAD_START_ROUTINE)lpMem就是告诉CreateThread函数,这个地址是线程的起始地址,也就是线程需要执行的代码的起始地址,然后CreateThread就会创建一个新线程,然后开始执行lpMem指向的代码
VirtualFree
1 | BOOL VirtualFree( |
- VirtualFree函数是一个用于释放、反提交或释放和反提交进程虚拟地址空间中的一块区域的函数
- VirtualFree函数的第二个参数dwSize表示要释放或反提交的内存区域的大小,以字节为单位
- 如果第三个参数dwFreeType是MEM_RELEASE,表示要释放整个由VirtualAlloc函数预留的区域,那么第二个参数dwSize必须是0,否则函数会失败
- 如果第三个参数dwFreeType是MEM_DECOMMIT,表示要反提交一块已提交的区域,那么第二个参数dwSize可以是任意值,如果是0,表示要反提交由VirtualAlloc函数分配的整个区域
总结
简单概括一下,要执行shellcode,一般需要做5件事:
1.申请一片内存区域(如果可以直接申请到可读可写可执行的话,跳过第2步)
2.改变内存区域的状态,使其可读可写可执行(这里是否必须要3者兼备作者目前不清楚)
3.将shellcode写入内存
4.执行shellcode
5.回收内存
本篇文章详细讲解了如何使用一个现成的shellcode,到此为止还没有正式涉及到有关免杀的知识和技巧,只能充其量算作免杀基础的win api的学习,不过有句话说得好:
千里之行始于足下
要打好基础,才能走得更远。